package org.tamal.spring;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.TypedQuery;
import javax.sql.DataSource;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

/**
 * This class contains utility methods.
 * @author Tamal Kanti Nath
 */
public final class Util {

    private static ApplicationContext context;
    private static DataSource dataSource;
    private static JdbcTemplate jdbcTemplate;
    private static Map<Thread, EntityManager> emMap = new HashMap<>();
    private static EntityManagerFactory entityManagerFactory;
    private static SessionFactory sessionFactory;

    static {
        context = new ClassPathXmlApplicationContext("META-INF/spring.xml");
        dataSource = context.getBean(DataSource.class);
        jdbcTemplate = new JdbcTemplate(dataSource);

        entityManagerFactory = Persistence.createEntityManagerFactory("hsqldb");

        Configuration configuration = new Configuration()
            .configure("META-INF/hibernate.cfg.xml");
        ServiceRegistry serviceRegistry = new ServiceRegistryBuilder()
            .applySettings(configuration.getProperties())
            .buildServiceRegistry();
        sessionFactory = configuration.buildSessionFactory(serviceRegistry);
    }

    private Util() {
        // Do nothing
    }

    /**
     * This method returns current session.
     * @return hibernate Session
     * @see SessionFactory#getCurrentSession()
     */
    public static Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    /**
     * This method returns an entity manager.
     * @return entity manager
     * @see EntityManagerFactory#createEntityManager()
     */
    public static EntityManager getEntityManager() {
        for (Map.Entry<Thread, EntityManager> entry : emMap.entrySet()) {
            if (!entry.getKey().isAlive()) {
                entry.getValue().close();
                emMap.remove(entry.getKey());
            }
        }
        EntityManager em = emMap.get(Thread.currentThread());
        if (em == null) {
            em = entityManagerFactory.createEntityManager();
            emMap.put(Thread.currentThread(), em);
        }
        return em;
    }

    /**
     * Returns a pure JDBC connection.
     * @return JDBC Connection
     * @throws SQLException if a database access error occurs
     * @see DataSource#getConnection()
     */
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    /**
     * Returns a Spring JDBC template.
     * @return JDBC Connection
     * @see DataSource#getConnection()
     */
    public static JdbcTemplate getJdbcTemplate() {
        return jdbcTemplate;
    }

    /**
     * Returns a Spring Application Context.
     * @return Application Context
     * @see ApplicationContext
     */
    public static ApplicationContext getContext() {
        return context;
    }

    /**
     * Returns an entity from named query with positional parameters.
     * @param <T> the List type
     * @param namedQuery the named HQL/SQL query
     * @param params the positional parameter values
     * @return an entity
     */
    public static <T> T findEntity(String namedQuery, Object... params) {
        Session session = getSession();
        Transaction t = session.beginTransaction();
        Query q = session.getNamedQuery(namedQuery);
        for (int i = 0; i < params.length; ++i) {
            q.setParameter(i, params[i]);
        }
        @SuppressWarnings("unchecked")
        T result = (T) q.uniqueResult();
        t.commit();
        return result;
    }

    /**
     * Returns an entity from named query with positional parameters.
     * @param <T> the List type
     * @param namedQuery the named JPQL/SQL query
     * @param cls the result class type
     * @param params the positional parameter values
     * @return an entity
     */
    public static <T> T findEntity(String namedQuery, Class<T> cls,
            Object... params) {
        EntityManager entityManager = getEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        TypedQuery<T> query = entityManager.createNamedQuery(namedQuery, cls);
        for (int i = 0; i < params.length; ++i) {
            query.setParameter(i, params[i]);
        }
        T result = query.getSingleResult();
        transaction.commit();
        return result;
    }

    /**
     * Returns a list of entities from query with positional parameters.
     * @param <T> the List type
     * @param query the named HQL/SQL query
     * @param params the named parameter name and value pairs
     * @return a list of entities
     */
    public static <T> List<T> findEntities(String query, Object... params) {
        Session session = getSession();
        Transaction t = session.beginTransaction();
        Query q = session.createQuery(query);
        for (int i = 0; i < params.length; i += 2) {
            q.setParameter((String) params[i], params[i + 1]);
        }
        @SuppressWarnings("unchecked")
        List<T> result = q.list();
        t.commit();
        return result;
    }

    /**
     * Returns a list of entities from query with positional parameters.
     * @param <T> the List type
     * @param query the named JPQL/SQL query
     * @param cls the result class type
     * @param params the positional parameter values
     * @return a list of entities
     */
    public static <T> List<T> findEntities(String query, Class<T> cls,
            Object... params) {
        EntityManager entityManager = getEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        TypedQuery<T> typedQuery = entityManager.createQuery(query, cls);
        for (int i = 0; i < params.length; i += 2) {
            typedQuery.setParameter((String) params[i], params[i + 1]);
        }
        List<T> result = typedQuery.getResultList();
        transaction.commit();
        return result;
    }

}
